﻿using System;
using System.Windows;
using System.Collections.Generic;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using Bling.DSL;
using Bling.Util;
using Bling.Linq;
using Bling.Properties;
using linq = Microsoft.Linq.Expressions;


namespace Bling.Arrays {
  public interface BaseArray1D<T> {
    int Count { get; }
    Expr<T> this[IntBl Index] { get; }
  }
  public interface BaseArray2D<T>  {
    int ColumnCount { get; }
    int RowCount { get; }
    Expr<T> this[IntBl x, IntBl y] { get; }
  }
  /// <summary>
  /// A one dimensional array of Bling values.
  /// </summary>
  /// <typeparam name="T">Type of values storied in array.</typeparam>
  public class Array1D<T> : BaseArray1D<T> {
    private readonly Func<IntBl, Expr<T>> F;
    /// <summary>
    /// Create a new array.
    /// </summary>
    /// <param name="Count">Number of elements in array.</param>
    /// <param name="F">Function that produces elements of array by (Index).</param>
    public Array1D(int Count, Func<IntBl, Expr<T>> F) {
      this.Count = Count; this.F = F;
    }
    /// <summary>
    /// Number of elements in array.
    /// </summary>
    public int Count { get; private set; }
    /// <summary>
    /// Access element of array.
    /// </summary>
    /// <param name="i">Index of element to access.</param>
    public Expr<T> this[IntBl i] { get { return F(i); } }
    /// <summary>
    /// Splice another array into this one.
    /// </summary>
    /// <param name="I">Index of splice.</param>
    /// <param name="other">Array to splice in.</param>
    public Array1D<T> Splice(IntBl I, Array1D<T> other) {
      var F0 = this.F;
      var F1 = other.F;
      return new Array1D<T>(Count + other.Count,
                 (i) => (i < I).Condition1<T>(F(i),
          (i < I + other.Count).Condition1<T>(F1(i - I), F0(i - other.Count))));
    }
    /// <summary>
    /// Transform this 1D array into a 2D array with specified number of columns. 
    /// The resulting 2D array is laid out horizontally row by row.
    /// </summary>
    /// <param name="ColumnCount">Number of columns in resulting 2D array.</param>
    public Array2D<T> Horizontal2D(int ColumnCount) {
      (Count % ColumnCount == 0).Assert();
      return new Array2D<T>(ColumnCount, Count / ColumnCount, (i, j) => F(i + j * ColumnCount));
    }
    /// <summary>
    /// Transform this 1D array into a 2D array with specified number of rows. 
    /// The resulting 2D array is laid out vertically column by column.
    /// </summary>
    /// <param name="RowCount"></param>
    /// <returns></returns>
    public Array2D<T> Vertical2D(int RowCount) {
      (Count % RowCount == 0).Assert();
      return new Array2D<T>(Count / RowCount, RowCount, (i, j) => F(i * RowCount + j));
    }
    /// <summary>
    /// Reverse the order of elements in this array.
    /// </summary>
    public Array1D<T> Reverse {
      get {
        var ret = new Array1D<T>(Count, i => F(Count - i - 1));
        return ret;
      }
    }
    /// <summary>
    /// Map each element of the array according to a function into a new array of the same length.
    /// </summary>
    /// <typeparam name="T0">Type of new array.</typeparam>
    /// <param name="F">Function that maps the elements.</param>
    public Array1D<T0> Map<T0>(Func<Expr<T>, Expr<T0>> F) {
      return new Array1D<T0>(Count, i => F(this.F(i)));
    }
    /// <summary>
    /// Map each element of the array according to a function into a new array of the same length.
    /// </summary>
    /// <param name="F">Function that maps the elements.</param>
    public Array1D<T> Map(Func<Expr<T>, Expr<T>> F) { return Map<T>(F); }
    /// <summary>
    /// Combine two same-length arrays together via a combining function.
    /// </summary>
    /// <typeparam name="T0">Element type of other array.</typeparam>
    /// <typeparam name="T1">Element type of resulting array.</typeparam>
    /// <param name="array">Other array to combine with this one.</param>
    /// <param name="F">Combining function.</param>
    public Array1D<T1> Combine<T0, T1>(Array1D<T0> array, Func<Expr<T>, Expr<T0>, Expr<T1>> F) {
      (array.Count == Count).Assert();
      return new Array1D<T1>(Count, i => F(this.F(i), array.F(i)));
    }
    /// <summary>
    /// Combine two same-length arrays together via a combining function.
    /// </summary>
    /// <param name="array">Other array to combine with this one.</param>
    /// <param name="F">Combining function.</param>
    public Array1D<T> Combine(Array1D<T> array, Func<Expr<T>, Expr<T>, Expr<T>> F) {
      return Combine<T, T>(array, F);
    }
  }
  public abstract class BrandedArray1D<T, BRAND, ARRAY>
    where BRAND : Brand<T, BRAND>
    where ARRAY : BrandedArray1D<T, BRAND, ARRAY> {
    private static Func<Array1D<T>, ARRAY> Brand0;
    protected static void Regster(Func<Array1D<T>, ARRAY> Brand0) { BrandedArray1D<T, BRAND, ARRAY>.Brand0 = Brand0; }
    public static ARRAY Brand(Array1D<T> Underlying) { return Brand0(Underlying); }
    public static implicit operator BrandedArray1D<T, BRAND, ARRAY>(Array1D<T> array) { return Brand(array); }
    public static implicit operator ARRAY(BrandedArray1D<T, BRAND, ARRAY> array) { return Brand(array.Underlying); }

    public readonly Array1D<T> Underlying;
    public BrandedArray1D(Array1D<T> Underlying) { this.Underlying = Underlying; }
    public int Count { get { return Underlying.Count; } }
    public BRAND this[IntBl i] { get { return Brand<T,BRAND>.ToBrand(Underlying[i]); } }

    public ARRAY Splice(IntBl I, ARRAY other) {
      return Brand(Underlying.Splice(I, other.Underlying));
    }
    public ARRAY Reverse {
      get { return Brand(Underlying.Reverse); }
    }
    public static ARRAY Map<T0,BRAND0,ARRAY0>(BrandedArray1D<T0,BRAND0,ARRAY0> array, Func<BRAND0, BRAND> F) where BRAND0 : Brand<T0,BRAND0> where ARRAY0 : BrandedArray1D<T0,BRAND0,ARRAY0> {
      return Brand(array.Underlying.Map<T>(value => F(Brand<T0,BRAND0>.ToBrand(value)).Underlying));
    }
    public ARRAY Map(Func<BRAND, BRAND> F) {
      return Map(this, F);
    }
    public static ARRAY Combine<T0, BRAND0, ARRAY0, T1, BRAND1, ARRAY1>(
      BrandedArray1D<T0, BRAND0, ARRAY0> array0,
      BrandedArray1D<T1, BRAND1, ARRAY1> array1, 
      Func<BRAND0, BRAND1, BRAND> F)
      where BRAND0 : Brand<T0, BRAND0>
      where ARRAY0 : BrandedArray1D<T0, BRAND0, ARRAY0>
      where BRAND1 : Brand<T1, BRAND1>
      where ARRAY1 : BrandedArray1D<T1, BRAND1, ARRAY1> {
      (array0.Count == array1.Count).Assert();
      var array2 = array0.Underlying.Combine<T1, T>(array1.Underlying, (v0, v1) => F(Brand<T0, BRAND0>.ToBrand(v0), Brand<T1, BRAND1>.ToBrand(v1)));
      return Brand(array2);
    }
    public ARRAY Combine(ARRAY other, Func<BRAND, BRAND, BRAND> F) {
      return Combine(this, other, F);
    }
  }
  public class AddArray1D<T, BRAND, ARRAY> : BrandedArray1D<T, BRAND, ARRAY>
    where BRAND : AddBl<T, BRAND>
    where ARRAY : AddArray1D<T, BRAND, ARRAY> {
    public AddArray1D(Array1D<T> Underlying) : base(Underlying) { }
    public static ARRAY operator +(AddArray1D<T, BRAND, ARRAY> op0, BRAND op1) {
      return op0.Map(value => value + op1);
    }
    public static ARRAY operator +(AddArray1D<T, BRAND, ARRAY> op0, ARRAY op1) {
      return op0.Combine(op1, (v0,v1) => v0 + v1);
    }
    public static ARRAY operator -(AddArray1D<T, BRAND, ARRAY> op0, BRAND op1) {
      return op0.Map(value => value - op1);
    }
    public static ARRAY operator -(AddArray1D<T, BRAND, ARRAY> op0, ARRAY op1) {
      return op0.Combine(op1, (v0,v1) => v0 - v1);
    }
  }
  public class MultXArray1D<T, BRAND, ARRAY> : AddArray1D<T, BRAND, ARRAY>
    where BRAND : MultXBl<T, BRAND>
    where ARRAY : MultXArray1D<T, BRAND, ARRAY> {
    public MultXArray1D(Array1D<T> Underlying) : base(Underlying) { }
    public static ARRAY operator *(MultXArray1D<T, BRAND, ARRAY> op0, DoubleBl op1) {
      return op0.Map(value => value * op1);
    }
    public static ARRAY operator *(MultXArray1D<T, BRAND, ARRAY> op0, DoubleArray1D op1) {
      return Combine(op0, op1, (v0, v1) => v0 * v1);
    }
    public static ARRAY operator /(MultXArray1D<T, BRAND, ARRAY> op0, DoubleBl op1) {
      return op0.Map(value => value / op1);
    }
    public static ARRAY operator /(MultXArray1D<T, BRAND, ARRAY> op0, DoubleArray1D op1) {
      return Combine(op0, op1, (v0, v1) => v0 / v1);
    }
  }
  public class MultArray1D<T, BRAND, ARRAY> : MultXArray1D<T, BRAND, ARRAY>
    where BRAND : MultBlValue<T, BRAND>
    where ARRAY : MultArray1D<T, BRAND, ARRAY> {
    public MultArray1D(Array1D<T> Underlying) : base(Underlying) { }
    public static ARRAY operator *(MultArray1D<T, BRAND, ARRAY> op0, BRAND op1) {
      return op0.Map(value => value * op1);
    }
    public static ARRAY operator *(MultArray1D<T, BRAND, ARRAY> op0, ARRAY op1) {
      return op0.Combine(op1, (v0, v1) => v0 * v1);
    }
    public static ARRAY operator /(MultArray1D<T, BRAND, ARRAY> op0, BRAND op1) {
      return op0.Map(value => value / op1);
    }
    public static ARRAY operator /(MultArray1D<T, BRAND, ARRAY> op0, ARRAY op1) {
      return op0.Combine(op1, (v0, v1) => v0 / v1);
    }
  }
  public class DoubleArray1D : MultXArray1D<double, DoubleBl, DoubleArray1D> {
    public DoubleArray1D(Array1D<double> Underlying) : base(Underlying) { }
    public static implicit operator DoubleArray1D(Array1D<double> Underlying) { return new DoubleArray1D(Underlying); }
    static DoubleArray1D() { Regster(v => v); }
  }
  public class PointArray1D : MultXArray1D<Point, PointBl, PointArray1D> {
    public PointArray1D(Array1D<Point> Underlying) : base(Underlying) { }
    public static implicit operator PointArray1D(Array1D<Point> Underlying) { return new PointArray1D(Underlying); }
    static PointArray1D() { Regster(v => v); }
    public static PointArray1D operator *(PointArray1D op0, PointBl op1) {
      return op0.Map(value => value * op1);
    }
    public static PointArray1D operator *(PointArray1D op0, PointArray1D op1) {
      return op0.Combine(op1, (v0, v1) => v0 * v1);
    }
    public static PointArray1D operator /(PointArray1D op0, PointBl op1) {
      return op0.Map(value => value / op1);
    }
    public static PointArray1D operator /(PointArray1D op0, PointArray1D op1) {
      return op0.Combine(op1, (v0, v1) => v0 / v1);
    }
  }
  public class Point3DArray1D : MultXArray1D<Point3D, Point3DBl, Point3DArray1D> {
    public Point3DArray1D(Array1D<Point3D> Underlying) : base(Underlying) { }
    public static implicit operator Point3DArray1D(Array1D<Point3D> Underlying) { return new Point3DArray1D(Underlying); }
    static Point3DArray1D() { Regster(v => v); }
    public static Point3DArray1D operator *(Point3DArray1D op0, Point3DBl op1) {
      return op0.Map(value => value * op1);
    }
    public static Point3DArray1D operator *(Point3DArray1D op0, Point3DArray1D op1) {
      return op0.Combine(op1, (v0, v1) => v0 * v1);
    }
    public static Point3DArray1D operator /(Point3DArray1D op0, Point3DBl op1) {
      return op0.Map(value => value / op1);
    }
    public static Point3DArray1D operator /(Point3DArray1D op0, Point3DArray1D op1) {
      return op0.Combine(op1, (v0, v1) => v0 / v1);
    }
  }



  /// <summary>
  /// A two dimensional array of Bling values.
  /// </summary>
  /// <typeparam name="T">Type of values storied in array.</typeparam>
  public class Array2D<T> : BaseArray2D<T> {
    private readonly Func<IntBl, IntBl, Expr<T>> F;
    /// <summary>
    /// Create a new 2D array.
    /// </summary>
    /// <param name="ColumnCount">Number of columns in array.</param>
    /// <param name="RowCount">Number of rows in array.</param>
    /// <param name="F">Function to produce array's elements by (Column, Row).</param>
    public Array2D(int ColumnCount, int RowCount, Func<IntBl,IntBl,Expr<T>> F) {
      this.RowCount = RowCount; this.ColumnCount = ColumnCount; this.F = F;
    }
    /// <summary>
    /// Access element of array.
    /// </summary>
    /// <param name="i">Column index of element.</param>
    /// <param name="j">Row index of element.</param>
    public Expr<T> this[IntBl i, IntBl j] { get { return F(i, j); } }
    /// <summary>
    /// Number of rows in array.
    /// </summary>
    public int RowCount { get; private set; }
    /// <summary>
    /// Number of columns in array.
    /// </summary>
    public int ColumnCount { get; private set; }
    /// <summary>
    /// Transform this array into a 1D array, horizontally row by row.
    /// </summary>
    public Array1D<T> Horizontal1D {
      get {
        return new Array1D<T>(ColumnCount * RowCount, (i) => F(i % ColumnCount, i / ColumnCount));
      }
    }
    /// <summary>
    /// Transform this array into a 1D array, vertically column by column.
    /// </summary>
    public Array1D<T> Vertical1D {
      get {
        return new Array1D<T>(ColumnCount * RowCount, (i) => F(i / RowCount, i % RowCount));
      }
    }
    /// <summary>
    /// Reverse the row order of this array.
    /// </summary>
    public Array2D<T> ReverseRows {
      get {
        return new Array2D<T>(ColumnCount, RowCount, (i, j) => F(i, RowCount - j - 1));
      }
    }
    /// <summary>
    /// Reverse the column order of this array.
    /// </summary>
    public Array2D<T> ReverseColumns {
      get {
        return new Array2D<T>(ColumnCount, RowCount, (i, j) => F(ColumnCount - i - 1, j));
      }
    }
    /// <summary>
    /// Swap row and column dimensions.
    /// </summary>
    public Array2D<T> Transpose {
      get {
        return new Array2D<T>(RowCount, ColumnCount, (i, j) => F(j, i));
      }
    }

    /// <summary>
    /// Splice the rows of another array into this one. 
    /// </summary>
    /// <param name="J">Row index where splice begins.</param>
    /// <param name="other">Other array to splice in, must have same number of columns as this array.</param>
    /// <returns>Combined array with additional rows.</returns>
    public Array2D<T> SpliceRows(IntBl J, Array2D<T> other) {
      (other.ColumnCount == ColumnCount).Assert();
      var F0 = this.F;
      var F1 = other.F;
      return new Array2D<T>(ColumnCount, RowCount + other.RowCount, 
                 (x,y) => (y < J).Condition1<T>(F0(x,y), 
         (y < J + other.RowCount).Condition1<T>(F1(x,y - J), F0(x, y - other.RowCount)))); 
    }
    /// <summary>
    /// Splice the columns of another array into this one. 
    /// </summary>
    /// <param name="I">Column index where splice begins.</param>
    /// <param name="other">Other array to splice in, must have same number of rows as this array.</param>
    /// <returns>Combined array with additional columns.</returns>
    public Array2D<T> SpliceColumns(IntBl I, Array2D<T> other) {
      (other.RowCount == RowCount).Assert();
      var F0 = this.F;
      var F1 = other.F;
      return new Array2D<T>(ColumnCount + other.ColumnCount, RowCount,
                   (x, y) => (x < I).Condition1<T>(F0(x, y),
         (x < I + other.ColumnCount).Condition1<T>(F1(x - I, y), F0(x - other.ColumnCount, y))));
    }
    /// <summary>
    /// Map each element of the array according to a function into a new array of the same dimensions.
    /// </summary>
    /// <typeparam name="T0">Type of new array.</typeparam>
    /// <param name="F">Function that maps the elements.</param>
    public Array2D<T0> Map<T0>(Func<Expr<int>,Expr<int>,Expr<T>, Expr<T0>> F) {
      var G = this.F;
      return new Array2D<T0>(ColumnCount, RowCount, (i,j) => F(i,j, G(i,j)));
    }
    /// <summary>
    /// Map each element of the array according to a function into a new array of the same dimensions.
    /// </summary>
    /// <param name="F">Function that maps the elements.</param>
    public Array2D<T> Map(Func<Expr<int>, Expr<int>, Expr<T>, Expr<T>> F) { return Map<T>(F); }

    /// <summary>
    /// Combine two same-dimensions arrays together via a combining function.
    /// </summary>
    /// <typeparam name="T0">Element type of other array.</typeparam>
    /// <typeparam name="T1">Element type of resulting array.</typeparam>
    /// <param name="array">Other array to combine with this one.</param>
    /// <param name="F">Combining function.</param>
    public Array2D<T1> Combine<T0, T1>(Array2D<T0> array, Func<Expr<T>, Expr<T0>, Expr<T1>> F) {
      (array.RowCount == RowCount && array.ColumnCount == ColumnCount).Assert();
      return new Array2D<T1>(ColumnCount, RowCount, (i,j) => F(this.F(i,j), array.F(i,j)));
    }
    /// <summary>
    /// Combine two same-dimensions arrays together via a combining function.
    /// </summary>
    /// <param name="array">Other array to combine with this one.</param>
    /// <param name="F">Combining function.</param>
    public Array2D<T> Combine(Array2D<T> array, Func<Expr<T>, Expr<T>, Expr<T>> F) {
      return Combine<T, T>(array, F);
    }
    private linq.Expression Loop<EVAL>(LinqEval<EVAL> eval, Func<Expr<T>, linq.Expression> F) where EVAL : Eval<EVAL> {
      return eval.For("i", ColumnCount, (i) => {
        LinqValue<int> I = new LinqValue<int>(i.Value);
        return eval.For("j", RowCount, (j) => {
          LinqValue<int> J = new LinqValue<int>(j.Value);
          var value = this[I, J];
          return F(value);
        });
      });
    }
  }
}